package com.nulldev.util.web.nTube.pages.streams;

import java.io.IOException;
import java.util.Map;

import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.concurrency.threadIt.v4.asyncTasks.AsyncTaskCallback;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpServer3.Response;
import com.nulldev.util.web.HttpServer3.legacyPages.v1.RequestType;
import com.nulldev.util.web.HttpServer3.pages.v3.ASyncHttpPage3;
import com.nulldev.util.web.URLUtils.URLQueryParser;
import com.nulldev.util.web.URLUtils.URLQueryParser.URLQueryParserOptions;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor.ExtractorType;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.nTube.components.video_queue.VideoQueueThread;
import com.nulldev.util.web.nTube.pages.GetVideoAsyncPage;
import com.nulldev.util.web.nTube.pages.GetVideoAsyncPage.TelemetryData;
import com.nulldev.util.web.nTube.pages.GetVideoAsyncPage.VideoRequestProfile;

public class ClientVideoRequestPage extends ASyncHttpPage3 {

	private static final String JSON_MIME = "application/json; charset=utf-8";

	private final ClientVideoRequestStream cvrs;
	private final VideoQueueThread VIDEO_QUEUE;

	public ClientVideoRequestPage(final ClientVideoRequestStream cvrs, final VideoQueueThread VIDEO_QUEUE) {
		super("/api/streams/video_request");
		super.setCachingMethod(CachingMethod.NO_CACHE);
		this.cvrs = Variables.requireNonNullObject(cvrs, "cvrs");
		this.VIDEO_QUEUE = Variables.requireNonNullObject(VIDEO_QUEUE, "VIDEO_QUEUE");
	}

	private static final Logger log = LoggerFactory.getLogger();
	protected final static IExecutorManager td = ExecutorManager.get();

	@SuppressWarnings("resource")
	@Override
	public void onFutureRequest(final Request _request, final ControllableFutureTask<Response> r) {
		final String args = _request.url();
		String body = null;
		if (_request.isType(RequestType.POST) && !_request.hasData()) {
			log.warn("Request didn't contain required data.");
			r.complete(new Response(400));
			return;
		}
		if (_request.hasData()) {
			try {
				body = new String(IOUtils.toByteArray(_request.getInputStream()));
			} catch (IOException e) {
				log.error("Failed to read body!", e);
			}
			if (!body.startsWith("{") && !body.endsWith("}")) {
				r.complete(new Response(400));
				return;
			}
		}
		UniJsonValue nextAPIBody = null;
		if (body != null && !body.isEmpty()) {
			nextAPIBody = UniJsonValue.parse(body);
		} else if (body != null && body.isEmpty()) {
			log.warn("Read body but it was empty...");
		} else if (body == null && _request.hasData()) {
			log.warn("Request had data and we didn't read it...");
		} else {
			nextAPIBody = UniJsonValue.object();
		}
		log.trace("Starting request_video command.");
		final Map<String, String> request = URLQueryParser.extract(args, URLQueryParserOptions.PREFER_EXPERIMENTAL, URLQueryParserOptions.TREAT_URL_AS_QUERY);
		final VideoRequestProfile vrp = new VideoRequestProfile();
		final int nextAPILevel = 3;
		if (nextAPIBody != null && !nextAPIBody.isNull() && nextAPILevel >= 2) {
			vrp.apply(nextAPIBody);
		} else {
			if (request != null && !request.isEmpty()) {
				vrp.apply(request);
			}
			if (nextAPIBody != null && !nextAPIBody.isNull()) {
				vrp.apply(nextAPIBody);
			}
		}
		if (!vrp.isValidRequest()) {
			r.complete(new Response(200, createStream(GetVideoAsyncPage.BAD_REQUEST_JSON)));
			return;
		}
		__yt(_request, request, vrp, nextAPIBody, nextAPILevel, r);
	}

	@SuppressWarnings("resource")
	private void __yt(final Request rq, final Map<String, String> request, final VideoRequestProfile vrp, final UniJsonValue nextAPIBody,
			final int nextAPILevel, final ControllableFutureTask<Response> r) {
		log.trace("Using YouTube extractor...");
		if (!nextAPIBody.hasValue("for")) {
			final UniJsonValue out = UniJsonValue.object(2);
			out.add("error", true);
			out.add("error_id", "NO_CLIENT_ID_PROVIDED");
			r.complete(new Response(200, out, out).setCustomMime(JSON_MIME));
			return;
		}
		final String forId = nextAPIBody.getString("for");
		final Executable<TelemetryData> telemetryData = td.asyncSubmit(() -> {
			if (nextAPIBody != null && !nextAPIBody.isNull() && nextAPIBody.contains("telemetry")) {
				final TelemetryData data = new TelemetryData(nextAPIBody.get("telemetry"));
				log.debug("telemetry: client=" + data.client + ",h264=" + data.supportsH264 + ",vp9=" + data.supportsVP9 + "[allowed=" + vrp.allowWebMs + "]"
						+ ",av1=" + data.supportsAV1 + "[allowed=" + vrp.enableAV1 + "],allowHDR=" + vrp.enableHDR);
				if (!data.supportsAV1 && !data.supportsH264 && data.supportsVP9) {
					vrp.openCodecsOnly = true;
					log.warn("telemetry: Data tells us that only VP9 is supported, forcing openCodecsOnly.");
				}
				return data;
			} else
				return null;
		});
		if ((rq.hasHeader("Save-Data") && rq.headerIs("Save-Data", "on")) || (rq.hasHeader("Sec-CH-Save-Data") && rq.headerIs("Sec-CH-Save-Data", "1"))) {
			vrp.dataSaving = true;
		}
		final IYouTubeExtractor ext = YouTubeExtractor.get(ExtractorType.get(vrp.extractionType));
		final Executable<ExtractedVideo> task;
		boolean successful_submission = false;
		if ((int) ext.getFlag(IYouTubeExtractorConstants.FLAG_EXTRACTOR_SUPPORTS_ABI_2_2) == IYouTubeExtractorConstants.FLAG_TRUE) {
			final Map<String, Object> map = FastMaps.uniMap(new Object[][]
				{
						{ IYouTubeExtractorConstants.MAP_TYPE_VIDEO_ID, vrp.videoID } });
			if (request.containsKey("list"))
				map.put(IYouTubeExtractorConstants.MAP_TYPE_PLAYLIST_STR, request.get("list"));
			if (request.containsKey("index"))
				map.put(IYouTubeExtractorConstants.MAP_TYPE_PLAYLIST_INDEX, MathUtil.safeParseInt(request.get("index"), -1));
			if (request.containsKey("clar") || nextAPIBody.bool("clar", false))
				map.put(IYouTubeExtractorConstants.MAP_TYPE_ENABLE_CLARIFICATIONS, true);
			if (vrp.systemInfo_UTC_TIMEZONE_OFFSET != -1)
				map.put(IYouTubeExtractorConstants.MAP_TYPE_UTC_TIMEZONE_OFFSET, vrp.systemInfo_UTC_TIMEZONE_OFFSET);
			if (GetVideoAsyncPage.ENABLE_LATE_EXTRACTIONS)
				map.put(IYouTubeExtractorConstants.MAP_TYPE_ENABLE_LATE_EXTRACTIONS, true);
			final Executable<ExtractedVideo> _task = ext.generateTask(map);
			try {
				successful_submission = this.VIDEO_QUEUE.submit(_task);
				task = _task;
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
			log.debug("[CVRS] task->dispatch (new)");
		} else {
			final UniJsonValue out = UniJsonValue.object(2);
			out.add("error_id", "ERROR_INCOMPATIBLE_EXTRACTOR");
			out.add("error_message", "This instance's extractor is using the pre-2.2 ABI, this is no longer supported.");
			r.complete(new Response(200, out, out).setCustomMime(JSON_MIME));
			return;
		}
		if (!successful_submission) {
			final UniJsonValue out = UniJsonValue.object(2);
			out.add("error_id", "ERROR_VIDEO_QUEUE_FULL");
			out.add("error_message", "This instance is currently very busy! Please try again later.");
			r.complete(new Response(200, out, out).setCustomMime(JSON_MIME));
			return;
		} else {
			final boolean task_accepted = task.addCallbackTask(new AsyncTaskCallback<ExtractedVideo>() {

				@Override
				public void onError(final Throwable e) {
					log.error("[ClientVideoRequestPage] onError::call", e);
					final UniJsonValue out = UniJsonValue.object();
					out.add("error_id", "ERROR_INTERNAL_EXCEPTION");
					out.add("error_message", "Hit '" + e.getClass().getSimpleName() + "' while fetching video.");
					r.complete(new Response(200, out, out).setCustomMime(JSON_MIME));
					return;
				}

				@Override
				public void onCompletion(final ExtractedVideo result) {
					log.debug("[ClientVideoRequestPage] onCompletion: " + result);
					if (result == null)
						throw new NullPointerException("result == null!");
					if (result.supportsLateExtraction())
						result.awaitCompletion();
					log.debug("[ClientVideoRequestPage] sendTo: " + forId);
					try {
						ClientVideoRequestPage.this.cvrs.sendTo(result, vrp, telemetryData.waitTillCompletion(), forId, ext.isLegacyBackend());
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}

			});
			final UniJsonValue out = UniJsonValue.object(3);
			out.add("error", !task_accepted);
			out.add("forId", forId);
			out.add("state", "pending");
			r.complete(new Response(200, out, out).setCustomMime(JSON_MIME));
			return;
		}
	}

}
